﻿using Sustainalytics.Claims;
using System;
using System.Collections.Generic;
using System.Linq;
using Sustainalytics.Entities;
using Sustainalytics.DynamicsCRMGateway;
using Sustainalytics.DynamicsCRMGateway.Entities;
using Sustainalytics.ClaimsProvisioningInCRM.Model;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Client;

namespace Sustainalytics.ClaimsProvisioningInCRM
{
    public class ClaimsProvisioning 
    {
        private IOperationClaimsRetriever _operationClaimsRetriever;
        private List<Sustainalytics.Entities.Account> _accounts;
        private ICrmRepository<Sustainalytics.DynamicsCRMGateway.Entities.Account> _accountRepo;
        private ICrmRepository<AccountFeature> _accountFeatureRepo;
        private ICrmRepository<Feature> _featureRepo;
        private ICrmRepository<Claim> _claimRepo;
        private CrmServiceContext _crmServiceContext;
        
        public ClaimsProvisioning(
            IOperationClaimsRetriever operationClaimsRetriever, 
            List<Sustainalytics.Entities.Account> accounts,
            ICrmRepository<Sustainalytics.DynamicsCRMGateway.Entities.Account> accountRepo, 
            ICrmRepository<AccountFeature> accountFeatureRepo,
            ICrmRepository<Feature> featureRepo, 
            ICrmRepository<Claim> claimRepo,
            CrmServiceContext crmServiceContext)
        {
            _operationClaimsRetriever = operationClaimsRetriever;
            _accounts = accounts;
            _accountRepo = accountRepo;
            _accountFeatureRepo = accountFeatureRepo;
            _featureRepo = featureRepo;
            _claimRepo = claimRepo;
            _crmServiceContext = crmServiceContext;
        }

        public void ProcessClaims()
        {
            StoreClaims(RetrieveClaims());
        }

        public Entities.Claims RetrieveClaims()
        {
            var claims = new Entities.Claims() { UserClaims = new List<UserClaim>()};

            claims.ClaimsMap = _operationClaimsRetriever.GetAllClaimMaps().ToList();
            
            _accounts.ForEach(acc => claims.UserClaims.Add(new UserClaim() 
            { 
                Account = acc.Name,
                Id = acc.Id,
                Claims = _operationClaimsRetriever.GetUserClaims(acc).ToList() 
            }));

            return claims;
        }

        const string _prefix = "Iustin_test__";
        private IEnumerable<Claim> _crmClaims;
        private List<Feature> _crmFeatures;
        private List<Sustainalytics.DynamicsCRMGateway.Entities.Account> _crmAccounts;
        private List<AccountFeature> _crmAccountFeatures;
        private List<Claim> _newClaims;
        private List<Feature> _newFeatures;
        private List<Sustainalytics.DynamicsCRMGateway.Entities.Account> _newAccounts;
        private List<Tuple<AccountFeature, Sustainalytics.DynamicsCRMGateway.Entities.Account, Feature>> _newFeatureAccounts;
        private List<Tuple<Claim, Feature>> _newClaimFeatureAssocs;

        private void CleanUp()
        {
            _accountRepo.Delete(_crmAccounts.Where(a=>a.Name.StartsWith(_prefix)).Select(a => a.AccountId.Value));
            _featureRepo.Delete(_crmFeatures.Where(f => f.Name.StartsWith(_prefix)).Select(f => f.FeatureId.Value));
            _claimRepo.Delete(_crmClaims.Where(c => c.Name.StartsWith(_prefix)).Select(c => c.ClaimId.Value));
        }

        public void StoreClaims(Entities.Claims accountsFeaturesClaims)
        {
            try
            {
                _crmClaims = _claimRepo.ReadAll();
                _crmFeatures = new List<Feature>(_featureRepo.ReadAll());
                _crmAccounts = new List<Sustainalytics.DynamicsCRMGateway.Entities.Account>(_accountRepo.ReadAll());
                _crmAccountFeatures = new List<AccountFeature>(_accountFeatureRepo.ReadAll());

                //CleanUp();

                _newClaims = new List<Claim>();
                _newFeatures = new List<Feature>();
                _newAccounts = new List<Sustainalytics.DynamicsCRMGateway.Entities.Account>();
                _newFeatureAccounts = new List<Tuple<AccountFeature, Sustainalytics.DynamicsCRMGateway.Entities.Account, Feature>>();
                _newClaimFeatureAssocs = new List<Tuple<Claim, Feature>>();

                var redisClaims = GroupByClaim(accountsFeaturesClaims);

                redisClaims.ForEach(cfa =>
                {
                    bool newClaim;
                    var claim = FindOrCreateClaim(cfa, out newClaim);
                    
                    //check its features
                    cfa.Features.ForEach(fn =>
                    {
                        if (newClaim || claim.Features == null || claim.Features.FirstOrDefault(f => f.Name == fn && (!f.IsPackage.HasValue || !f.IsPackage.Value)) == null) //add feature for this claim if doesn't exist
                        {
                            bool newEntity;
                            var feature = FindOrCreateFeature(fn, out newEntity);
                            _newClaimFeatureAssocs.Add(new Tuple<Claim, Feature>(claim, feature));
                        }
                    });

                    cfa.Accounts.ForEach(an =>
                    {
                        bool newAccount;
                        var account = FindOrCreateEntity<Sustainalytics.DynamicsCRMGateway.Entities.Account>(_crmAccounts, _newAccounts, an, out newAccount);

                        LinkClaimToAccount(claim, account, newClaim || newAccount);
                    });
                });
            
                SaveToCRM();
                Console.WriteLine("Claims provisioning succeeded!");
                Console.ReadKey();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
                Console.WriteLine(ex.StackTrace);
                Console.ReadKey();
            }
        }

        private void SaveToCRM()
        {
//            try
  //          {
                _claimRepo.Create(_newClaims);
                _featureRepo.Create(_newFeatures);
                _accountRepo.Create(_newAccounts);
                var featureClaimRelationship = new Microsoft.Xrm.Sdk.Relationship("new_new_feature_new_claim");

                _newClaimFeatureAssocs.GroupBy(cf => cf.Item1, cf => cf.Item2, (key, g) => new { Claim = key, Features = g.ToList() }).ToList()
                    .ForEach(cf => _crmServiceContext.Associate(
                        Claim.EntityLogicalName,
                        cf.Claim.ClaimId.Value,
                        featureClaimRelationship,
                        new Microsoft.Xrm.Sdk.EntityReferenceCollection(cf.Features.Select(f => new EntityReference(Feature.EntityLogicalName, f.FeatureId.Value)).ToList())));

                _accountFeatureRepo.Create(_newFeatureAccounts.Select(x=>x.Item1));
                _newFeatureAccounts.ForEach(x => { x.Item1.Account = x.Item2; x.Item1.Feature = x.Item3; });

                _accountFeatureRepo.Update(_newFeatureAccounts.Select(x => x.Item1));
//            }
//            finally
            //{
                //_accountFeatureRepo.Delete(_newFeatureAccounts.Select(x => x.Item1.AccountFeatureId.Value));
                //_accountRepo.Delete(_newAccounts.Select(a => a.AccountId.Value));
                //_featureRepo.Delete(_newFeatures.Select(f => f.FeatureId.Value));
                //_claimRepo.Delete(_newClaims.Select(c => c.ClaimId.Value));
            //}
        }

        private Claim FindOrCreateClaim(ClaimFeaturesAccounts cfa, out bool newClaim)
        {
            newClaim = false;
            var cl = _crmClaims.FirstOrDefault(c => c.Name == cfa.Claim.Type && c.Value == cfa.Claim.Value);
            if (cl == null)//new claim:
            {
                cl = new Claim()
                {
                    Name = /*_prefix +*/ cfa.Claim.Type,
                    Value = cfa.Claim.Value,
                    ClaimId = Guid.NewGuid()
                };
                _newClaims.Add(cl);
                newClaim = true;
            }
            return cl;
        }

        private Feature FindOrCreateFeature(string name, out bool newEntity)
        {
            newEntity = false;
            var entity = _crmFeatures.FirstOrDefault(x => x.Name == name && (!x.IsPackage.HasValue || !x.IsPackage.Value));
            if (entity == null)//new entity
            {
                entity = new Feature() { Name = /*_prefix +*/ name, Id = Guid.NewGuid() };
                _crmFeatures.Add(entity);
                _newFeatures.Add(entity);
                newEntity = true;
            }
            return entity;

        }

        private T FindOrCreateEntity<T>(List<T> crmEntities, List<T> newEntities, string name, out bool newEntity) where T : CrmEntity, INameable, new()
        {
            newEntity = false;
            var entity = crmEntities.FirstOrDefault(x => x.Name == name);
            if (entity == null)//new entity
            {
                entity = new T() { Name = /*_prefix +*/ name, Id = Guid.NewGuid() };
                crmEntities.Add(entity);
                newEntities.Add(entity);
                newEntity = true;
            }
            return entity;
        }

        private void LinkClaimToAccount(Claim claim, DynamicsCRMGateway.Entities.Account foundAccount, bool dontCheckLinks)
        {
            List<Feature> features = new List<Feature>(_newClaimFeatureAssocs.Where(cf => cf.Item1.Name == claim.Name).Select(x => x.Item2));
            if (claim.Features != null) //read claim
            {
                features.AddRange(claim.Features.Where(f => !f.IsPackage.HasValue || !f.IsPackage.Value));
            }
            if (dontCheckLinks || !AreFeatureAccountLinks(features, foundAccount))
            {//link all features to account!
                foreach (var feature in features)
                {
                    //if (!IsAlreadyAddedFeatureAccount(foundAccount.Id, feature.Id))
                    //{
                    _newFeatureAccounts.Add(new Tuple<AccountFeature, Sustainalytics.DynamicsCRMGateway.Entities.Account, Feature>(
                        new AccountFeature() { Id = Guid.NewGuid() },
                        foundAccount,
                        feature));
                    //}
                }
            }
        }

        /*private bool IsAlreadyAddedFeatureAccount(Guid accountId, Guid featureId)
        {
             if(_newFeatureAccounts.Find(x=>x.Item2.Id == accountId && x.Item3.Id==featureId)!=null)
                 return true;

             if (_crmAccountFeatures.Find(fa => {
                 return (fa.Feature != null && fa.Feature.Id == featureId && fa.Account !=null && fa.Account.Id == accountId);
             }) != null)
                return true;

            return false;
        }*/

        private bool AreFeatureAccountLinks(IEnumerable<Feature> features, DynamicsCRMGateway.Entities.Account account)
        {
            //check direct links between feature and account
            foreach (var feature in features)
            {
                var accFeature = _crmAccountFeatures.Find(af => af.Account != null && af.Account.AccountId == account.AccountId && af.Feature!=null && af.Feature.FeatureId == feature.FeatureId);
                if (accFeature != null)
                    return true;
            }

            //check the account to package feature links
            var childFeatures = account.AccountFeatures.Where(af => (af.Feature.IsPackage.HasValue && af.Feature.IsPackage.Value)).SelectMany(af=>af.Feature.ChildFeatures);
            if (childFeatures.Count() > 0)
            {
                //intersect features and childFeatures
                return features.Intersect(childFeatures, new FeatureComparer()).Count()>0;
            }

            return false;
        }

        class FeatureComparer : IEqualityComparer<Feature>
        {
            // Features are equal if their names are equal. 
            public bool Equals(Feature x, Feature y)
            {

                //Check whether the compared objects reference the same data. 
                if (Object.ReferenceEquals(x, y)) return true;

                //Check whether any of the compared objects is null. 
                if (Object.ReferenceEquals(x, null) || Object.ReferenceEquals(y, null))
                    return false;

                //Check whether the features' names are equal. 
                return x.Name == y.Name;
            }

            // If Equals() returns true for a pair of objects  
            // then GetHashCode() must return the same value for these objects. 

            public int GetHashCode(Feature feature)
            {
                //Check whether the object is null 
                if (Object.ReferenceEquals(feature, null)) return 0;

                //Get hash code for the Name field if it is not null. 
                int hashProductName = feature.Name == null ? 0 : feature.Name.GetHashCode();

                //Calculate the hash code for the product. 
                return hashProductName;
            }
        }

        private List<ClaimFeaturesAccounts> GroupByClaim(Entities.Claims accountsFeaturesClaims)
        {
            var claims = new List<ClaimFeaturesAccounts>();
            accountsFeaturesClaims.ClaimsMap.ForEach(perm => perm.Claims.ForEach(c =>
            {
                var foundCFA = claims.Find(cfa => cfa.Claim.Type == c.Type && cfa.Claim.Value == c.Value);
                if (foundCFA != null)
                    foundCFA.Features.Add(perm.Permission);
                else
                    claims.Add(new ClaimFeaturesAccounts()
                    {
                        Claim = c,
                        Features = new List<string>() { perm.Permission },
                        Accounts = new List<string>()
                    });

            }));

            accountsFeaturesClaims.UserClaims.ForEach(user => user.Claims.ForEach(c =>
            {
                var foundCFA = claims.Find(cfa => cfa.Claim.Type == c.Type && cfa.Claim.Value == c.Value);
                if (foundCFA != null)
                    foundCFA.Accounts.Add(user.Account);
                else
                    claims.Add(new ClaimFeaturesAccounts()
                    {
                        Claim = c,
                        Accounts = new List<string>() { user.Account },
                        Features = new List<string>()
                    });
            }));

            return claims;
        }
    }
}
